package com.atguigu.gulimall.seckill.service.impl;

import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.json.JSONUtil;
import com.atguigu.common.constant.SeckillConstant;
import com.atguigu.common.utils.R;
import com.atguigu.common.vo.MemberResponseVo;
import com.atguigu.common.vo.SeckillSessionWithRelationSkuVo;
import com.atguigu.common.vo.mq.SecKillOrderTo;
import com.atguigu.gulimall.seckill.feign.CouponFeignService;
import com.atguigu.gulimall.seckill.feign.ProductFeignService;
import com.atguigu.gulimall.seckill.interceptor.LoginUserInterceptor;
import com.atguigu.gulimall.seckill.service.SeckillService;
import com.atguigu.gulimall.seckill.to.SeckillSkuRedisTo;
import com.atguigu.gulimall.seckill.vo.SkuInfoVo;
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
import jodd.util.StringUtil;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RSemaphore;
import org.redisson.api.RedissonClient;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.BeanUtils;
import org.springframework.data.redis.core.BoundHashOperations;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.util.Date;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

/**
 * @create: 2024/9/16
 * @author: gyhe10
 * @description:
 **/

@Slf4j
@Service
public class SeckillServiceImpl implements SeckillService {

    @Resource
    private CouponFeignService couponFeignService;
    @Resource
    private StringRedisTemplate stringRedisTemplate;
    @Resource
    private ProductFeignService productFeignService;
    @Resource
    private RedissonClient redissonClient;
    @Resource
    private RabbitTemplate rabbitTemplate;

    /**
     * 上架最近三天需要秒杀的商品
     */
    @Override
    public void uploadSeckillSkuLatest3Days() {
        // 远程调用 coupon 服务，获取到最近三天需要秒杀的商品信息
        R r = couponFeignService.getLatest3DaysSession();
        if (r.getCode() == 0) {
            Object data = r.get("data");
            if (data != null) {
                List<SeckillSessionWithRelationSkuVo> sessionList = JSONUtil.toList(JSONUtil.toJsonStr(data), SeckillSessionWithRelationSkuVo.class);
                // 缓存秒杀活动信息
                cacheSessionInfos(sessionList);
                // 缓存秒杀活动涉及的商品信息
                cacheSessionSkuInfos(sessionList);
            }
        }
    }

    /**
     * 缓存秒杀活动信息
     *
     * @param sessionList
     */
    private void cacheSessionInfos(List<SeckillSessionWithRelationSkuVo> sessionList) {
        sessionList.forEach(session -> {
            long startTime = session.getStartTime().getTime();
            long endTime = session.getEndTime().getTime();
            String key = SeckillConstant.SESSION_CACHE_PREFIX + startTime + "_" + endTime;
            // 如果 Redis 中没有此数据，才进行缓存
            if (Boolean.FALSE.equals(stringRedisTemplate.hasKey(key))) {
                // 将获取到的 sessionId_skuId 集合作为缓存秒杀活动信息时的 value
                List<String> value = session.getRelationSkus().stream().map(item -> item.getPromotionSessionId() + "_" + item.getSkuId()).collect(Collectors.toList());
                // 缓存活动信息
                stringRedisTemplate.opsForList().leftPushAll(key, value);
            }
        });
    }

    /**
     * 缓存秒杀活动的商品信息
     *
     * @param sessionList
     */
    private void cacheSessionSkuInfos(List<SeckillSessionWithRelationSkuVo> sessionList) {
        // 秒杀活动的商品信息使用 Redis 中的 Hash 数据结构
        BoundHashOperations<String, Object, Object> hashOps = stringRedisTemplate.boundHashOps(SeckillConstant.SKU_SEC_KILL_CACHE_PREFIX);

        sessionList.forEach(session -> session.getRelationSkus().forEach(relationSku -> {
            // 在设计秒杀系统时，引入随机码是一种防止机器人批量请求、提高秒杀公平性的策略。随机码可以作为一种验证机制，确保只有合法的用户能够参与秒杀活动。
            String randomCode = UUID.randomUUID().toString().replace("-", "");
            // 当前秒杀活动中每个关联商品的 ID
            Long skuId = relationSku.getSkuId();
            // 缓存商品信息的 key
            String key = relationSku.getPromotionSessionId() + "_" + skuId;
            if (Boolean.FALSE.equals(hashOps.hasKey(key))) {
                // 缓存商品信息的 value，包含商品基本信息、秒杀信息
                SeckillSkuRedisTo seckillSkuRedisTo = new SeckillSkuRedisTo();
                // 远程调用 product 服务，获取秒杀商品的基本信息
                R r = productFeignService.getSkuInfo(skuId);
                if (r.getCode() == 0) {
                    Object data = r.get("skuInfo");
                    if (data != null) {
                        seckillSkuRedisTo.setSkuInfo(JSONUtil.toBean(JSONUtil.toJsonStr(data), SkuInfoVo.class));
                    }
                }
                // 秒杀信息
                BeanUtils.copyProperties(relationSku, seckillSkuRedisTo);
                // 设置秒杀商品的开始时间、结束时间
                seckillSkuRedisTo.setStartTime(session.getStartTime().getTime());
                seckillSkuRedisTo.setEndTime(session.getEndTime().getTime());
                // 设置秒杀商品随机码
                seckillSkuRedisTo.setRandomCode(randomCode);
                // 将秒杀商品数据转为 JSON
                String skuInfoJson = JSONUtil.toJsonStr(seckillSkuRedisTo);
                // 缓存秒杀活动的商品信息到 Redis
                hashOps.put(key, skuInfoJson);
                // 使用 Redisson 提供的分布式信号量作为秒杀商品的库存
                RSemaphore semaphore = redissonClient.getSemaphore(SeckillConstant.SKU_STOCK_SEMAPHORE + randomCode);
                semaphore.trySetPermits(relationSku.getSeckillCount());
            }
        }));
    }

    /**
     * 获取当前时间可以参与的秒杀活动及其关联商品信息
     *
     * @return
     */
    @Override
    public List<SeckillSkuRedisTo> getCurrentSeckillSkus() {
        // 当前时间可以参与的所有秒杀活动
        // 表示从 1970 年 1 月 1 日 00:00:00 UTC 到现在的时间毫秒数。
        long time = new Date().getTime();
        // 缓存秒杀活动信息时的 key 就是前缀 + 秒杀活动开始时间毫秒数 + "_" + 秒杀活动结束时间毫秒数
        // 根据秒杀活动前缀，查询所有的 key
        Set<String> keys = stringRedisTemplate.keys(SeckillConstant.SESSION_CACHE_PREFIX + "*");
        for (String key : keys) {
            String replace = key.replace(SeckillConstant.SESSION_CACHE_PREFIX, "");
            String[] s = replace.split("_");
            long start = Long.parseLong(s[0]);
            long end = Long.parseLong(s[1]);
            if (time >= start && time <= end) {
                // 获取当前秒杀活动关联的所有商品信息
                List<String> range = stringRedisTemplate.opsForList().range(key, -100, 100);
                BoundHashOperations<String, String, String> hashOps = stringRedisTemplate.boundHashOps(SeckillConstant.SKU_SEC_KILL_CACHE_PREFIX);
                List<String> list = hashOps.multiGet(range);
                if (CollectionUtil.isNotEmpty(list)) {
                    return list.stream().map(item -> {
                        SeckillSkuRedisTo seckillSkuRedisTo = JSONUtil.toBean(item, SeckillSkuRedisTo.class);
                        // 如果当前秒杀商品在预告阶段，其参与的秒杀活动还没有开始，则需要设置随机码为 null
                        // 如果当前秒杀商品在秒杀阶段，则需要保留其随机码
                        // seckillSkuRedisTo.setRandomCode(null);
                        return seckillSkuRedisTo;
                    }).collect(Collectors.toList());
                }
            }
        }
        return null;
    }

    /**
     * 根据商品 skuId 查询秒杀信息
     *
     * @param skuId
     * @return
     */
    @Override
    public SeckillSkuRedisTo getSkuSeckillInfo(Long skuId) {
        // 获取所有需要参与秒杀的商品信息的 key
        BoundHashOperations<String, String, String> hashOps = stringRedisTemplate.boundHashOps(SeckillConstant.SKU_SEC_KILL_CACHE_PREFIX);
        Set<String> keys = hashOps.keys();
        if (CollectionUtil.isNotEmpty(keys)) {
            // 正则匹配
            String reg = "\\d_" + skuId;
            for (String key : keys) {
                if (Pattern.matches(reg, key)) {
                    String jsonStr = hashOps.get(key);
                    SeckillSkuRedisTo seckillSkuRedisTo = JSONUtil.toBean(jsonStr, SeckillSkuRedisTo.class);
                    // 如果当前秒杀商品在预告阶段，其参与的秒杀活动还没有开始，则需要设置随机码为 null
                    if (new Date().getTime() < seckillSkuRedisTo.getStartTime() || new Date().getTime() > seckillSkuRedisTo.getEndTime()) {
                        seckillSkuRedisTo.setRandomCode(null);
                    }
                    return seckillSkuRedisTo;
                }
            }
        }
        return null;
    }

    // TODO 优化：缓存秒杀商品时，应当根据秒杀活动结束时间设置缓存过期时间
    // TODO 优化：秒杀商品库存的信号量应当设置过期时间，避免商品库存的信号量一直存在，导致内存溢出
    // TODO 优化：这里没有考虑收货地址的处理
    @Override
    public String kill(String killId, String key, Integer num) {

        long start = System.currentTimeMillis();

        MemberResponseVo memberResponseVo = LoginUserInterceptor.loginUser.get();
        // 获取当前秒杀商品详情
        BoundHashOperations<String, String, String> hashOps = stringRedisTemplate.boundHashOps(SeckillConstant.SKU_SEC_KILL_CACHE_PREFIX);
        String jsonStr = hashOps.get(killId);
        if (StringUtil.isNotEmpty(jsonStr)) {
            SeckillSkuRedisTo seckillSkuRedisTo = JSONUtil.toBean(jsonStr, SeckillSkuRedisTo.class);
            // 校验时间是否在秒杀活动时间范围内
            if (new Date().getTime() >= seckillSkuRedisTo.getStartTime() && new Date().getTime() <= seckillSkuRedisTo.getEndTime()) {
                // 校验随机码是否正确
                if (seckillSkuRedisTo.getRandomCode().equals(key) && killId.equals(seckillSkuRedisTo.getPromotionSessionId() + "_" + seckillSkuRedisTo.getSkuId())) {
                    // 校验购买数量是否合理
                    if (num <= seckillSkuRedisTo.getSeckillLimit()) {
                        // 校验当前用户是否已经秒杀过了该商品，幂等性：防止重复秒杀，秒杀成功在 Redis 使用 SETNX 命令添加一条记录
                        // key 是 userId_promotionSessionId_skuId
                        // value 是购买数量
                        // 过期时间是 秒杀活动结束时间 - 当前时间
                        long ttl = seckillSkuRedisTo.getEndTime() - new Date().getTime();
                        String redisKey = memberResponseVo.getId() + "_" + killId;
                        Boolean ifAbsent = stringRedisTemplate.opsForValue().setIfAbsent(redisKey, num.toString(), ttl, TimeUnit.MILLISECONDS);
                        if (ifAbsent) {
                            // 向 Redis 中添加记录成功，说明当前用户没有秒杀过该商品，继续执行秒杀逻辑
                            // 扣减库存信号量
                            RSemaphore semaphore = redissonClient.getSemaphore(SeckillConstant.SKU_STOCK_SEMAPHORE + key);
                            // acquire() 方法如果没有可用的许可，则当前线程会被阻塞，直到有其他线程释放了许可，高并发情况下不建议使用
                            // tryAcquire() 方法，如果没有可用的许可，则立即返回 false，不会阻塞当前线程
                            // semaphore.acquire(num);
                            boolean b = semaphore.tryAcquire(num);
                            if (b) {
                                String orderSn = IdWorker.getTimeId();
                                // 秒杀成功，发送消息给 MQ，创建订单
                                SecKillOrderTo secKillOrderTo = new SecKillOrderTo();
                                secKillOrderTo.setOrderSn(orderSn);
                                secKillOrderTo.setPromotionSessionId(seckillSkuRedisTo.getPromotionSessionId());
                                secKillOrderTo.setSkuId(seckillSkuRedisTo.getSkuId());
                                secKillOrderTo.setSeckillPrice(seckillSkuRedisTo.getSeckillPrice());
                                secKillOrderTo.setNum(num);
                                secKillOrderTo.setMemberId(memberResponseVo.getId());
                                rabbitTemplate.convertAndSend("order-event-exchange", "order.seckill.order", secKillOrderTo);

                                long end = System.currentTimeMillis();
                                log.info("秒杀成功，耗时：{} ms", end - start);

                                return orderSn;
                            }
                        }
                    }
                }
            }
        }
        return null;
    }
}
